/*
    Dashboard.js
    Copyright (C) 2010, Donald W. Griffin
    All Rights Reserved
*/
Ext.namespace("Wdg");

/**
This class manages a Dashboard. A Dashboard is defined by the Parts it provides. A
Part is an object that creates components (essentially a realized Part). For example,
an RSS feed Part creates the components necessary to manage an RSS feed when given
such as the RSS feed URL. The Part can be used to create any number of Component
instances.

It is generally not a good idea to put too much logic in the Part class. Rather, it
is best to view the Part as a simple factory for a Component where the core logic
exists.

It is important to note that even though this class derives from Panel, it is not
expected that the user will create items directly. Rather, this class translates
the dashboard's state into child component instances using its Parts.
*/
Wdg.Dashboard = Ext.extend(Ext.Panel, function () {
// private
    var base;

    function steal (from) {
        if (!from)
            return 0;
        var fw = from.getSize().width;
        var w = fw / 2;
        if (from.minWidth)
            w = Math.min(fw - from.minWidth, w);
        from.setSize(fw - w);
        return w;
    }

// public
return {
    layout : 'splitcolumn',
    autoScroll : false,
    cls : 'ux-dashboard',
    defaultType : 'dashboardcolumn',
    insertColLeftCls: "ux-dashboard-insert-col-left",
    insertColRightCls: "ux-dashboard-insert-col-right",
    xtype: "dashboard",

    minColWidth: 100,
    itemPadding: 5,

    /**
    This parts object maps from the type name to a Part or a config for a Part.
    For example,
    
        parts: {
            PartOne: {
                create: function (cfg) { return new MyComponent(cfg); }
            },
            PartTwo: new PartTwo() // where PartTwo extends Part
        }

    In the above, PartOne is a config object. It is passed to the Part ctor. But
    PartTwo is a constructed Part object of some user-defined type.
    */
    // parts: { PartType: ... },

    /**
    This property defines the initial dashboard state. The contents of this object
    are replaced if the dashboard is stateful. This object (either user provided or
    loaded from state) drives the creation of the dashboard's items.

        dashboardState: [
            { // Column 1
                width: 200,
                items: [
                    { type: "PartOne", cfg: { } },
                    { type: "PartOne", cfg: { } }
                ]
            },
            { // Column 2
                width: 400,
                items: [
                    { type: "PartTwo", cfg: { } }
                ]
            }
        ]
    */
    // dashboardState: [ ... ],

    initComponent : function(){
        base = Wdg.Dashboard.superclass;
        base.initComponent.call(this);

        if (!this.layoutConfig)
            this.layoutConfig = {};
        this.layoutConfig.padding = this.itemPadding;

        var parts = {};

        // Convert any parts that are just configs into proper Part objects:
        Ext.iterate(this.parts, function (key, part) {
            if (!part.isPart)
                part = new Wdg.Dashboard.Part(part);

            part.dashboard = this;
            part.type = key;
            parts[key] = part;
        }, this);

        this.parts = parts;

        this.addEvents({
            validatedrop:true,
            beforedragover:true,
            dragover:true,
            beforedrop:true,
            drop:true
        });
    },

    initEvents : function(){
        base.initEvents.call(this);
        this.dd = new Wdg.Dashboard.DropZone(this, this.dropConfig);
    },

    applyState : function(state){
        if (state)
            this.dashboardState = state;
    },

    doDrop: function (drop, pos) {
        if (this.fireEvent('validatedrop', drop) === false ||
                this.fireEvent('beforedrop', drop) === false) {
            return false;
        }

        var dd = drop.source, c = drop.column, left = drop.leftEdge;
        dd.proxy.getProxy().remove();
        dd.panel.el.dom.parentNode.removeChild(dd.panel.el.dom);

        if (left || drop.rightEdge) {
            var ci = drop.columnIndex + (left ? 0 : 1), n = this.items.getCount();
            var lhs = ci ? this.items.itemAt(ci-1) : null;
            var rhs = (ci < n) ? this.items.itemAt(ci) : null;
            var w = steal(lhs) + steal(rhs);
            var col = { minWidth: Math.max(this.minColWidth, dd.panel.minWidth||0),
                        width: w, items: [] };
            if (ci < n)
                this.insert(ci, col);
            else
                this.add(col);
            c = this.items.itemAt(ci);
            c.add(dd.panel);
            this.doLayout();
        } else {
            if (pos !== false) {
                if(c == dd.panel.ownerCt && (c.items.items.indexOf(dd.panel) <= pos)){
                    pos++;
                }
                c.insert(pos, dd.panel);
            }else{
                c.add(dd.panel);
            }

            c.doLayout();
        }

        this.removeEmptyColumns();
        this.fireEvent('drop', drop);
        return true;
    },

    getState : function(){
        var state = [];

        this.items.each(function (col) {
            var items = [];

            col.items.each(function (it) {
                items.push(this.pickle(it));
            }, this);

            state.push({ width: col.getWidth(), items: items });
        }, this);

        return state;
    },

    makeItems : function () {
        var columns = [];

        var state = this.dashboardState, pad = this.itemPadding || "";
        if (pad)
            pad = "padding:" + pad + "px 0px 0px 0px;"; // TRBL

        for (var i = 0; i < state.length; ++i) { // foreach column
            var colState = state[i];
            var col = { minWidth: this.minColWidth, width: colState.width, items: [] };

            for (var j = 0; j < colState.items.length; ++j) { // foreach item
                var pc = this.createItem(colState.items[j]);
                if (pad)
                    pc.style = pad;

                col.items.push(pc);
                if (pc.minWidth && col.minWidth < pc.minWidth)
                    col.minWidth = pc.minWidth;
            }

            columns.push(col);
        }

        this.add.apply(this, columns);
    },

    createItem : function (pickle) {
        var cfg = pickle.cfg || {};
        var part = this.parts[pickle.type];
        var it = part.createInstance(cfg);

        return it;
    },

    pickle: function (item) {
        var p = item.part;
        var ret = { type: p.type, cfg: p.pickle(item) };
        return ret;
    },

    render : function () {
        if (!this.items)
            this.makeItems();

        return base.render.apply(this, arguments);
    },

    removeEmptyColumns : function () {
        var remove = [];

        this.items.each(function (col)
        {
            if (col.items.length == 0)
                remove.push(col);
        });

        if (remove.length == 0)
            return;

        Ext.each(remove, function (col)
        {
            this.items.remove(col);
            col.destroy();
        }, this);

        this.doLayout();
    },

    beforeDestroy : function() {
        if(this.dd){
            this.dd.unreg();
        }
        base.beforeDestroy.call(this);
    }
};

}()); // Dashboard

Wdg.Dashboard.Part = Ext.extend(Object, {
    isPart: true,

    constructor: function (cfg) {
        this.lastId = 0;
        Ext.apply(this, cfg);
    },

    createInstance: function (cfg) {
        var n = ++this.lastId;
        var id = (n > 1) ? (this.type + n) : this.type;
        id = this.dashboard.id + '_' + id;

        var t = Ext.applyIf({
                    id: id,
                    dashboard: this.dashboard,
                    part: this,
                    partCfg: cfg,
                    mkId: function (sfx) {
                        return id + '_' + sfx;
                    }
                },
                cfg);

        var cmp = this.create(t);
        return cmp;
    },

    pickle: function (item) {
        var cfg = Ext.apply({}, item.partCfg);
        return cfg;
    }
});

Wdg.Dashboard.Column = Ext.extend(Ext.Container, {
    layout : 'anchor',
    defaultType : 'dashboardpanel',
    cls : 'ux-dashboard-column',
    xtype: "dashboardcolumn"
});

Wdg.Dashboard.Panel = Ext.extend(Ext.Panel, {
    anchor : '100%',
    frame : true,
    collapsible : true,
    draggable : true,
    cls : 'ux-dashboard-panel',
    xtype : 'dashboardpanel'
});

(function (T) {
    for (var i = 0; i < arguments.length; ++i) {
        T = arguments[i];
        Ext.reg(T.prototype.xtype, T);
    }
})(
    Wdg.Dashboard,
    Wdg.Dashboard.Column,
    Wdg.Dashboard.Panel
);

//-----

Wdg.Dashboard.DropZone = Ext.extend(Ext.dd.DropTarget, {

    ddScrollConfig : {
        vthresh: 50,
        hthresh: -1,
        animate: true,
        increment: 200
    },

    constructor : function(dashboard, cfg){
        this.dashboard = dashboard;
        Ext.dd.ScrollManager.register(dashboard.body);
        Wdg.Dashboard.DropZone.superclass.constructor.call(this, dashboard.bwrap.dom, cfg);
        dashboard.body.ddScrollConfig = this.ddScrollConfig;
    },

    createEvent : function(dd, e, data, col, c, pos, ledge, redge){
        return {
            dashboard: this.dashboard,
            panel: data.panel,
            columnIndex: col,
            column: c,
            position: pos,
            data: data,
            source: dd,
            rawEvent: e,
            leftEdge: ledge,
            rightEdge: redge,
            status: this.dropAllowed
        };
    },

    notifyOver : function(dd, e, data){
        var xy = e.getXY(), dashboard = this.dashboard, px = dd.proxy, proxy = px.getProxy();

        // case column widths
        if(!this.grid){
            this.grid = this.getGrid();
            if (dashboard.itemPadding) {
                proxy.setStyle("margin-top", dashboard.itemPadding+"px");
            }
        }

        // handle case scroll where scrollbars appear during drag
        var cw = dashboard.body.dom.clientWidth;
        if(!this.lastCW){
            this.lastCW = cw;
        }else if(this.lastCW != cw){
            this.lastCW = cw;
            dashboard.doLayout();
            this.grid = this.getGrid();
        }

        // determine column
        var col = 0, xs = this.grid.columnX, cmatch = false, ledge = false, redge = false;
        for(var len = xs.length; col < len; col++){
            if(xy[0] < (xs[col].x + xs[col].w)){
                cmatch = true;
                var dx = xy[0] - xs[col].x, rem;
                if (dx >= 0 && dx < xs[col].w / 4) {
                    ledge = true;
                    proxy.addClass(dashboard.insertColLeftCls);
                    rem = dashboard.insertColRightCls;
                } else if (dx > xs[col].w * 3 / 4) {
                    redge = true;
                    proxy.addClass(dashboard.insertColRightCls);
                    rem = dashboard.insertColLeftCls;
                } else {
                    rem = [dashboard.insertColLeftCls, dashboard.insertColRightCls];
                }
                proxy.removeClass(rem);
                break;
            }
        }

        // no match, fix last index
        if(!cmatch){
            col--;
        }

        // find insert position
        var p, match = false, pos = 0,
            c = dashboard.items.itemAt(col),
            items = c.items.items, overSelf = false;

        for (len = items.length; pos < len; pos++){
            p = items[pos];
            var h = p.el.getHeight();
            if(h === 0){
                overSelf = true;
            }
            else if((p.el.getY()+(h/2)) > xy[1]){
                match = true;
                break;
            }
        }

        pos = (match && p ? pos : c.items.getCount()) + (overSelf ? -1 : 0);
        
        var overEvent = this.createEvent(dd, e, data, col, c, pos, ledge, redge);

        if (dashboard.fireEvent('validatedrop', overEvent) !== false &&
            dashboard.fireEvent('beforedragover', overEvent) !== false){

            if (ledge || redge)
                proxy.setWidth(25);
            else // make sure proxy width is fluid
                proxy.setWidth('auto');

            var m = redge ? (xs[col].w - 25) : 0;
            proxy.setStyle("margin-left", m + "px");

            if(p){
                px.moveProxy(p.el.dom.parentNode, match ? p.el.dom : null);
            }else{
                px.moveProxy(c.el.dom, null);
            }

            this.lastPos = {c: c, col: col, p: overSelf || (match && p) ? pos : false,
                            ledge: ledge, redge: redge};
            this.scrollPos = dashboard.body.getScroll();

            dashboard.fireEvent('dragover', overEvent);
        }

        return overEvent.status;
    },

    notifyOut : function(){
        delete this.grid;
    },

    notifyDrop : function(dd, e, data){
        delete this.grid;
        if(!this.lastPos){
            return;
        }

        var lp = this.lastPos, c = lp.c, col = lp.col, pos = lp.p;

        var dropEvent = this.createEvent(dd, e, data, col, c,
                                (pos !== false ? pos : c.items.getCount()),
                                lp.ledge, lp.redge);

        if (this.dashboard.doDrop(dropEvent, pos)) {
            // scroll position is lost on drop, fix it
            var st = this.scrollPos.top;
            if(st){
                var d = this.dashboard.body.dom;
                setTimeout(function(){
                    d.scrollTop = st;
                }, 10);
            }
        }

        delete this.lastPos;
    },

    // internal cache of body and column coords
    getGrid : function(){
        var box = this.dashboard.bwrap.getBox();
        box.columnX = [];
        this.dashboard.items.each(function(c){
             box.columnX.push({x: c.el.getX(), w: c.el.getWidth()});
        });
        return box;
    },

    // unregister the dropzone from ScrollManager
    unreg: function() {
        //Ext.dd.ScrollManager.unregister(this.dashboard.body);
        Wdg.Dashboard.DropZone.superclass.unreg.call(this);
    }
});

//----

Wdg.SplitColumnLayout = Ext.extend(Ext.layout.ContainerLayout, {

    type: "splitcolumn",

    monitorResize: true,

    extraCls: "x-column",

    scrollOffset : 0,

    padding: 5,

    splitSize: 5,

    targetCls: "x-column-layout-ct",

    vfit : false,

    isValidParent : function(c, target) {
        return c.getPositionEl().dom.parentNode == this.innerCt.dom;
    },

    onLayout : function(ct, target) {
        var ci = ct.items, ic = this.innerCt;
        if (!ic) {
            this.innerCt = ic = target.createChild({ cls: "x-column-inner" });
            ic.createChild({ cls: "x-clear" });
        }

        this.removeSplitters();
        this.renderAll(ct, ic); // doesn't get along with splitters in the dom...
        this.addSplitters(ci);

        var size = target.getViewSize(true);
        var w = size.width - this.scrollOffset, h = size.height;
        if (w < 1 && h < 1)
            return;

        ic.setWidth(w);

        var sum = 0, ss = this.splitSize, ps = this.padding, pw = w - 2*ps, widths = [];
        ci.each(function (it) {
            var cw = it.getSize().width;
            sum += cw;
            if (widths.length)
                pw -= ss;
            widths.push(cw);
        });

        pw /= sum;
        ci.each(function (it, index) {
            // prev box collapse when dragging last panel in column:
            it.el.dom.style.minHeight = "1px";

            var cw = Math.floor(widths[index] * pw);

            this.setColSize(it, cw, h);
            if (index)
                this.splitters[index-1].el.setSize(ss, h);

            it.el.setStyle("margin-left", index ? "0" : (this.padding + "px"));
        }, this);
    },

    getSplitMaxSize : function () {
        // NOTE: "this" is the SplitBar!
        var ret = this._rhs.getWidth();
        ret += this._lhs.getWidth();
        ret -= this._lhs.minWidth;
        return ret;
    },

    getSplitMinSize : function () {
        // NOTE: "this" is the SplitBar!
        return this._rhs.minWidth;
    },

    onMoveSplit : function (split, newSize) {
        var lhs = split._lhs, rhs = split._rhs;
        var lw = lhs.getWidth(), rw = rhs.getWidth();
        rhs.setSize(newSize);
        lhs.setSize(lw - (newSize - rw));
        return false;
    },

    addSplitters : function (items) {
        this.splitters = [];

        var getMax = this.getSplitMaxSize, getMin = this.getSplitMinSize;
        var prev = items.itemAt(0), SplitBar = Ext.SplitBar;

        items.each(function (it, index) {
            if (!index)
                return;

            var splitEl = this.innerCt.createChild({
                id: it.getId() + '-xsplit',
                cls: "x-column x-layout-split x-layout-split-east",
                style: "position: static;",
                html: "&#160;"
            });

            var sb = new SplitBar(splitEl.dom, it.el, SplitBar.HORIZONTAL, SplitBar.RIGHT);

            sb.tickSize = 8;
            sb.getMaximumSize = getMax;
            sb.getMinimumSize = getMin;
            sb.on("beforeapply", this.onMoveSplit, this);
            sb._lhs = prev;
            sb._rhs = it;
            prev = it;

            sb.el.insertBefore(it.el);

            this.splitters.push(sb);
        }, this);
    },

    removeSplitters : function () {
        if (this.splitters) {
            Ext.each(this.splitters, function (sb) {
                sb.destroy(true);
            });
            this.splitters = null;
        }
    },

    setColSize : function (c, w, h) {
        if (this.vfit)
            c.setSize(w, h);
        else
            c.setSize(w);
    }
});

Wdg.regLayout(Wdg.SplitColumnLayout);
